Skip to content

Add cheat code support (retro_cheat_set / retro_cheat_reset)#113

Open
JoeMatt wants to merge 1 commit intolibretro:masterfrom
Provenance-Emu:claude/add-cheat-codes-PzxlL
Open

Add cheat code support (retro_cheat_set / retro_cheat_reset)#113
JoeMatt wants to merge 1 commit intolibretro:masterfrom
Provenance-Emu:claude/add-cheat-codes-PzxlL

Conversation

@JoeMatt
Copy link
Copy Markdown
Collaborator

@JoeMatt JoeMatt commented Apr 19, 2026

Wires up the previously-stubbed libretro cheat hooks so RetroArch users can apply cheat codes to Jaguar games. The parser accepts the common Pro Action Replay / GameShark-style hex format (6- or 8-digit address followed by a 2-, 4-, or 8-digit byte/word/long value) with optional space / colon / hyphen / dot separators, and supports multi-code strings joined with '+' or newlines. Cheats are re-applied after every frame so games that continuously overwrite the patched location are held to the cheat value.

The parser and list management are extracted into src/cheat.{c,h} so they are unit-testable without the full emulator. test/test_cheat.c contains 130 assertions covering every accepted format length, all separator styles, rejection of malformed input, list add/remove/toggle, multi-code strings, capacity clamping, byte/word/long memory application against a simulated big-endian address space, a frame-loop scenario simulating an infinite-lives cheat overriding a "CPU" that rewrites the value each frame, and real-world-shaped PAR examples.

The tests are plain C99, link only src/cheat.c (no BIOS or ROM required), and are run in CI across every matrix target (Linux GCC, Linux Clang, Linux aarch64, macOS arm64, Windows MSYS2) so cheat support is verified on every supported platform.

This PR implements a complete cheat code engine for the Jaguar emulator, supporting the Pro Action Replay (PAR) / GameShark-style hex format commonly used for Jaguar game cheats.

Summary

Adds a self-contained, unit-tested cheat parsing and application system that integrates with the libretro frontend's cheat interface. The implementation is modular and testable in isolation from the main emulator.

Key Changes

  • New cheat engine (src/cheat.c, src/cheat.h):

    • cheat_parse_one(): Parses individual cheat codes in multiple formats (6+2, 6+4, 8+4, 6+8, 8+8 hex digit combinations)
    • Flexible separator handling: space, colon, hyphen, dot, or no separator between address and value
    • Case-insensitive hex parsing with automatic 24-bit address masking
    • cheat_list_set(): Manages cheat entries with support for multi-code strings ('+' and newline-separated)
    • cheat_list_apply(): Applies cheats via callback, enabling memory-agnostic operation
    • Support for byte, word, and long writes with big-endian ordering matching the Jaguar bus
  • Comprehensive unit tests (test/test_cheat.c):

    • 449 lines of self-contained tests covering parser validation, list management, multi-code strings, memory application, and end-to-end scenarios
    • Tests real-world cheat code shapes from public Jaguar cheat databases
    • Simulates a 16 MB address space with proper big-endian writes
    • Includes "fake ROM" scenario where a game continuously overwrites a patched location
  • Integration with libretro core (libretro.c):

    • Implements retro_cheat_reset() and retro_cheat_set() callbacks
    • Applies cheats every frame via cheat_apply_all() after CPU execution
    • Cleans up cheats on game unload
  • Build system updates:

    • Added src/cheat.c to Makefile.common source list
    • Added make test target to build and run unit tests
    • Integrated unit tests into CI/CD pipeline (.github/workflows/c-cpp.yml)
    • Updated .gitignore for test artifacts

Implementation Details

The parser accepts codes like:

  • 00003D00 FFFF (PAR canonical: 8-digit address + 4-digit word value)
  • F03200 7F (6-digit address + 2-digit byte value)
  • 100000 CAFEBABE (6-digit address + 8-digit long value)
  • Separators (space, colon, hyphen, dot) are optional and interchangeable

Multi-code strings are supported for RetroArch .cht files:

00003D00 FFFF+00100000 BEEF\n00200000 CAFEBABE

The cheat application uses a callback pattern (cheat_write_fn) to remain independent of the memory implementation, making the core logic fully unit-testable without the emulator.

https://claude.ai/code/session_019diR7d7dZwUqdzB6J9TEic

Wires up the previously-stubbed libretro cheat hooks so RetroArch users
can apply cheat codes to Jaguar games. The parser accepts the common
Pro Action Replay / GameShark-style hex format (6- or 8-digit address
followed by a 2-, 4-, or 8-digit byte/word/long value) with optional
space / colon / hyphen / dot separators, and supports multi-code strings
joined with '+' or newlines. Cheats are re-applied after every frame so
games that continuously overwrite the patched location are held to the
cheat value.

The parser and list management are extracted into src/cheat.{c,h} so
they are unit-testable without the full emulator. test/test_cheat.c
contains 130 assertions covering every accepted format length, all
separator styles, rejection of malformed input, list add/remove/toggle,
multi-code strings, capacity clamping, byte/word/long memory
application against a simulated big-endian address space, a frame-loop
scenario simulating an infinite-lives cheat overriding a "CPU" that
rewrites the value each frame, and real-world-shaped PAR examples.

The tests are plain C99, link only src/cheat.c (no BIOS or ROM
required), and are run in CI across every matrix target (Linux GCC,
Linux Clang, Linux aarch64, macOS arm64, Windows MSYS2) so cheat
support is verified on every supported platform.
@JoeMatt JoeMatt self-assigned this Apr 19, 2026
Copilot AI review requested due to automatic review settings April 19, 2026 03:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a self-contained cheat-code engine and wires it into the libretro core so RetroArch can enable/disable cheats and have them re-applied every frame (to override games that continuously rewrite patched memory).

Changes:

  • Introduces src/cheat.{c,h} implementing cheat parsing, list management, and callback-based application.
  • Integrates libretro retro_cheat_set / retro_cheat_reset and applies cheats after each JaguarExecuteNew() frame.
  • Adds C99 unit tests plus build/CI plumbing to compile and run them.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test/test_cheat.c New self-contained unit tests covering parsing, list ops, and application via a simulated memory bus.
src/cheat.h Public API/types for cheat parsing, list management, and callback-based memory writes.
src/cheat.c Implementation of parser, list add/remove/toggle behavior, and apply loop.
libretro.c Hooks libretro cheat callbacks into the new cheat engine and applies cheats every frame.
Makefile.common Adds src/cheat.c to the core build sources.
Makefile Adds make test target and cleans the built cheat test binary.
.gitignore Ignores the cheat unit test binary under test/.
.github/workflows/c-cpp.yml Runs the cheat unit tests in CI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +76 to +77
-o test_cheat test/test_cheat.c src/cheat.c
./test_cheat
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This CI step builds the test binary as ./test_cheat in the repo root, while the Makefile target builds test/test_cheat (and .gitignore ignores that path). To keep things consistent and avoid stray artifacts, consider invoking make test here, or at least outputting to test/test_cheat and running that.

Suggested change
-o test_cheat test/test_cheat.c src/cheat.c
./test_cheat
-o test/test_cheat test/test_cheat.c src/cheat.c
./test/test_cheat

Copilot uses AI. Check for mistakes.
Comment thread src/cheat.c
Comment on lines +91 to +95
if (!list || !code)
return;

cheat_list_remove_index(list, index);
if (!enabled)
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cheat_list_set()'s header contract says that when enabled=false it should remove existing entries for index, but the implementation returns early when code == NULL before calling cheat_list_remove_index(). This means callers that pass NULL (or disable without a code string) will be unable to turn cheats off. Consider removing the !code early-return (or moving the NULL check after the enabled branch) so removal always happens regardless of code.

Suggested change
if (!list || !code)
return;
cheat_list_remove_index(list, index);
if (!enabled)
if (!list)
return;
cheat_list_remove_index(list, index);
if (!enabled || !code)

Copilot uses AI. Check for mistakes.
Comment thread src/cheat.c
Comment on lines +26 to +34
for (; *code && n < sizeof(buf) - 1; code++)
{
if (hex_digit((char)*code) >= 0)
buf[n++] = *code;
else if (*code != ' ' && *code != '\t' && *code != ':' &&
*code != '-' && *code != '.')
return false;
}
buf[n] = '\0';
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cheat_parse_one() silently truncates input once it has collected 31 hex digits (buf size limit) without checking whether the original string still has additional hex digits. That can cause overlong inputs to be accepted after truncation (e.g., the first 16 digits form a valid code but extra digits follow). Consider detecting overflow (if the loop exits because n hit the limit and there are still characters left) and returning false, or cap the number of hex digits at 16 and reject anything beyond.

Copilot uses AI. Check for mistakes.
Comment thread test/test_cheat.c
*
* Build & run (from repo root):
* cc -O2 -Wall -std=c99 -I src -I libretro-common/include \
* -o test_cheat test/test_cheat.c src/cheat.c && ./test_cheat
Copy link

Copilot AI Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build/run command in the file header produces an output binary named test_cheat in the repo root, but the Makefile/CI use test/test_cheat and .gitignore only ignores test/test_cheat. Following these instructions will leave an unignored binary in the root directory. Consider updating the example command to match the make test target (output to test/test_cheat and run that).

Suggested change
* -o test_cheat test/test_cheat.c src/cheat.c && ./test_cheat
* -o test/test_cheat test/test_cheat.c src/cheat.c && ./test/test_cheat

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants